/*
 * OpenParts
 * A dynamic-loading components framework for GWT
 * Copyright (C) 2011 Christophe Bouthier  [chris{AT}binary-gastronome{DOT}fr]
 *
 * This work is partially based on work I have done at INRIA (http://www.inria.fr) 
 * in the context of the Qualipso European Project (http://qualipso.org/),
 * The work done at INRIA is Copyright (C) 2006-2011 INRIA
 *
 * This work is distributed under the LGPL version 3
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation. See the GNU
 * Lesser General Public License in LGPL.txt for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package fr.openparts.OpenParts.client;


import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;

import fr.openparts.OpenParts.logging.client.OPLogger;
import fr.openparts.OpenParts.logging.client.OPLogging;

/**
 * The OPBinding class represent the binding service for the parts. It binds parts callbacks, so that registered parts get even from the event bus, and can be
 * called to display their views. It is a singleton, which shared resource can only be accessed through {@link OPShell#opBindingSharedInstance()}.
 * <p>
 * OPBinding propose several kinds of binding:
 * <ul>
 * <li>event bus binding</li>
 * <li>view binding</li>
 * </ul>
 * 
 * Event bus binding is done by registering a callback that will be called as soon as a parts dispatch an event on the event bus, with a call to the
 * {@link OPShell#dispatchEvent(String)}. An event is just a String.
 * <p>
 * It is also possible to send an event only to a specific service, and not to all registered services. This is done with a call to
 * {@link OPShell#dispatchEventTo(String, String)}. The registration of event handler is the same than for a standard event handling.
 * <p>
 * View binding is done by registering the service name that the part represent (the role of the part), then the resources that are managed by this part (the
 * kind of objects managed by the part), and then, for each resource, a view factory for each possible view, identified by the action represented by the view.
 * <p>
 * All binding are done through annotations in {@link fr.openparts.OpenParts.client.annotations}, with the use of {@link OPBinder} (see this
 * class for more details).
 * <p>
 * OPBinding is a JavaScriptObject, that is a real javascript object mapped to Java thanks to GWT overlay absolutely fantastic technique. It is mainly an hash
 * linked directly to the window global variable. It register event bus callback binding through its "listeners" hash, and its services/resources/views through
 * imbricated hash: services[serviceName].resources[resourceName].actions[actionName]. It also contains references to the parts loader (in charge of loading
 * parts), and to the scheduler, in charge of scheduling and executing commands (for loading parts, display views, and dispatching events).
 * 
 * @author Christophe Bouthier [chris{AT}binary-gastronome{DOT}fr]
 * @creation.date 3 December 2009
 */
public final class OPBinding extends JavaScriptObject {
    
    protected static final OPLogger loggerBinding = OPLogging.getLoggerForDomain("binding");
    protected static final OPLogger loggerEventDispatch = OPLogging.getLoggerForDomain("eventDispatch");

    /**
     * Returns an initialized binding.
     * 
     * @return an initialized binding.
     */
    final native static OPBinding bindingFactory() /*-{
        var binding = {};
        binding.listeners = {};
        binding.services = {};
        binding.globalcounter = 0;
        binding.scheduler = null;
        binding.partsLoader = null;
        return binding;
    }-*/;

    /**
     * Protected constructor, necessary because it's a JavaScriptObject.
     */
    protected OPBinding() {}

    /**
     * Return the number of event bus listeners already binded.
     * 
     * @return the number of listeners on the event bus. Can be 0.
     */
    public final native int getNumberOfEventBusListeners() /*-{
        var count = 0;
        for (var l in this.listeners) {
            if (this.listeners.hasOwnProperty(l)) {
                ++count;
            }
        }
        return count;
    }-*/;

    /**
     * Increments and returns the global counter for slots. Each call to this method increments the counter.
     * 
     * @return the global counter for slots.
     */
    public final native int getGlobalSlotCounter() /*-{
        this.globalcounter += 1;
        return this.globalcounter;
    }-*/;

    /**
     * Returns an array of names of all services binded with a view.
     * 
     * @return an array of names of all services binded with a view.
     */
    public final String[] getServicesNames() {
        final JsArrayString servicesNames = getJsServicesNames();
        String[] result = new String[servicesNames.length()];
        for (int i = 0; i < servicesNames.length(); i++) {
            result[i] = servicesNames.get(i);
        }
        return result;
    }

    /**
     * Returns a native js array containing all services names.
     * 
     * @return a native js array containing all services names. Can be empty, cannot be null.
     */
    private final native JsArrayString getJsServicesNames() /*-{
        var names = new Array();
        var count = 0;

        for (var p in this.services) {
            if (this.services.hasOwnProperty(p)) {
                names[count] = p;
                ++count;
            }
        }

        return names;
    }-*/;

    /**
     * Returns the binded service for the given service name, or null if there is no such service binded.
     * 
     * @param service
     *            the service name searched for. Shouldn't be null.
     * @return the given binded service, or null if there is no such service binded.
     */
    public final OPBindingService getService(final String service) {
        assert service != null : "parameter service shouldn't be null";

        return getServiceImpl(service);
    }

    /**
     * Real implementation for returning the binded service for the given service name, or null if there is no such service binded.
     * 
     * @param service
     *            the service name searched for.
     * @return the given binded service, or null if there is no such service binded.
     */
    private final native OPBindingService getServiceImpl(final String service) /*-{
        if (! this.services[service]) {
            return null;
        }
        return this.services[service];
    }-*/;

    /**
     * Returns the binded resource for the given service and resource name, or null if there is no such resource binded.
     * 
     * @param service
     *            the service name. Shouldn't be null.
     * @param resource
     *            the resource name. Shouldn't be null.
     * @return the binded resource for the given service and resource name, or null if there is no such resource binded.
     */
    public final OPBindingResource getResource(final String service, final String resource) {
        assert service != null : "parameter service shouldn't be null";
        assert resource != null : "parameter resource shouldn't be null";

        final OPBindingService bindedService = getService(service);
        if (bindedService == null) {
            return null;
        }

        return bindedService.getResource(resource);
    }

    /**
     * Returns the binded action for the given service, resource, and action name, or null if there is no such view binded.
     * 
     * @param service
     *            the service name. Shouldn't be null.
     * @param resource
     *            the resource name. Shouldn't be null.
     * @param action
     *            the action name. Shouldn't be null.
     * @return the binded action for the given service, resource, and action name, or null if there is no such view binded.
     */
    public final OPBindingAction getAction(final String service, final String resource, final String action) {
        assert service != null : "parameter service shouldn't be null";
        assert resource != null : "parameter resource shouldn't be null";
        assert action != null : "parameter action shouldn't be null";

        final OPBindingResource bindedResource = getResource(service, resource);
        if (bindedResource == null) {
            return null;
        }

        return bindedResource.getAction(action);
    }

    /**
     * Bind the given service. If another service with the same name was already binded, just change the service.
     * 
     * @param service
     *            the service to bind. Shouldn't be null.
     */
    public final void bind(final OPBindingService service) {
        assert service != null : "parameter service shouldn't be null";
        loggerBinding.log("Binding service " + service.getServiceName());
        
        bindImpl(service);
    }

    /**
     * Real implementation of binding the given service. If another service with the same name was already binded, just change the service.
     * 
     * @param service
     *            the service to bind.
     */
    private final native void bindImpl(final OPBindingService service) /*-{
        this.services[service.name] = service;
    }-*/;

    /**
     * Bind the given scheduler. If another scheduler was already binded, just change the scheduler.
     * 
     * @param schedulerBinding
     *            the binding object encapsulating the scheduler. Shouldn't be null.
     */
    public final void bind(final OPSchedulerBinding schedulerBinding) {
        assert schedulerBinding != null : "parameter schedulerBinding shouldn't be null";

        bindImpl(schedulerBinding);
    }

    /**
     * Real implementation of binding the scheduler, taking in parameter the binding encapsulation object. If another scheduler was already binded, just change
     * the scheduler.
     * 
     * @param schedulerBinding
     *            the binding object encapsulating the scheduler.
     */
    private final native void bindImpl(final OPSchedulerBinding schedulerBinding) /*-{
        this.scheduler = schedulerBinding;
    }-*/;

    /**
     * Returns the binded scheduler. Return null if the scheduler hasn't been binded.
     * 
     * @return the binded scheduler, or null if there is no scheduler binded.
     */
    protected final native OPSchedulerBinding getSchedulerBinding() /*-{
        if (! this.scheduler) {
            return null;
        }
        return this.scheduler;
    }-*/;

    /**
     * Schedule the given command.
     * 
     * @param command
     *            the command to schedule for execution. Shouldn't be null.
     */
    public final void scheduleCommand(final OPCommand command) {
        assert command != null : "parameter command shouldn't be null";

        final OPSchedulerBinding scheduler = getSchedulerBinding();
        assert scheduler != null : "Cannot find scheduler. Did you load your main view with OPContainer?";

        scheduler.scheduleCommand(command);
    }
    
    /**
     * Bind the given partsLoader. If another parts loader was already binded, just change the parts loader.
     * 
     * @param partsLoaderBinding
     *            the binding object encapsulating the parts loader. Shouldn't be null.
     */
    public final void bind(final OPPartsLoaderBinding partsLoaderBinding) {
        assert partsLoaderBinding != null : "parameter partsLoaderBinding shouldn't be null";
        
        bindImpl(partsLoaderBinding);
    }

    /**
     * Real implementation of binding the parts loader, taking in parameter the binding encapsulation object. If another parts loader was already binded, just
     * change the parts loader.
     * 
     * @param partsLoaderBinding
     *            the binding object encapsulating the parts loader.
     */
    private final native void bindImpl(final OPPartsLoaderBinding partsLoaderBinding) /*-{
        this.partsLoader = partsLoaderBinding;
    }-*/;

    /**
     * Returns the binded parts loader. Return null if the parts loader hasn't been binded.
     * 
     * @return the binded parts loader, or null if there is no parts loader binded.
     */
    protected final native OPPartsLoaderBinding getPartsLoaderBinding() /*-{
        if (! this.partsLoader) {
            return null;
        }
        return this.partsLoader;
    }-*/;

    /**
     * Execute the given loading request in order for the service to be loaded.
     * 
     * @param request
     *            the loading request. Shouldn't be null.
     */
    public final void executeLoadingRequest(final OPPartLoadingRequest request) {
        assert request != null : "parameter request shouldn't be null";

        final OPPartsLoaderBinding partsLoader = getPartsLoaderBinding();
        assert partsLoader != null : "Cannot find parts loader. Did you load your main view with OPContainer?";

        partsLoader.executeLoadingRequest(request);
    }

    /**
     * Notify the parts loader that the part with the given service name finished loading and binding itself.
     * 
     * @param service
     *            the name of the service loaded. Shouldn't be null.
     */
    final void notifyLoadingCompleted(final String service) {
        assert service != null : "parameter service shouldn't be null";

        final OPPartsLoaderBinding partsLoader = getPartsLoaderBinding();
        assert partsLoader != null : "Cannot find parts loader. Did you load your main view with OPContainer?";

        partsLoader.notifyLoadingCompleted(service);
    }

    /**
     * Ask the parts loader if the part with the given service name is already loaded.
     * 
     * @param service
     *            the service name of the part to check for. Shouldn't be null.
     */
    final boolean isPartLoaded(final String service) {
        assert service != null : "parameter service shouldn't be null";

        final OPPartsLoaderBinding partsLoader = getPartsLoaderBinding();
        assert partsLoader != null : "Cannot find parts loader. Did you load your main view with OPContainer?";

        return partsLoader.isPartLoaded(service);
    }

    /**
     * Dispatch the given event and parameters to all listeners. The event shouldn't be null. Shouldn't be called directly, called by {@link OPCommandFactory}.
     * 
     * @param event
     *            the event to dispatch to all listeners. Shouldn't be null.
     * @param paramsJS
     *            the params associated with the event. Can be empty, can be null.
     */
    final void dispatchEvent(final String event, final OPParamsJS paramsJS) {
        assert event != null : "parameter event shouldn't be null";
        loggerEventDispatch.log("Dispatching event " + event + (paramsJS == null ? " with no params" : "with params " + paramsJS.getDescription()));

        dispatchEventImpl(event, paramsJS);
    }

    /**
     * Real implementation of event dispatching to all listeners.
     * 
     * @param event
     *            the event to dispatch to all listeners. Shouldn't be null.
     * @param params
     *            the params associated with the event. Can be empty, can be null.
     */
    private final native void dispatchEventImpl(final String event, final OPParamsJS params) /*-{
        for (var name in this.listeners) {
            this.listeners[name](event, params);
        }
    }-*/;

    /**
     * Dispatch the given event and parameters to the specified listener. The event and service name shouldn't be null. Return false if there
     * is no such listener. Shouldn't be called directly, called by {@link OPCommandFactory}.
     * 
     * @param service
     *            the name of the listener to dispatch the message.
     * @param event
     *            the event to dispatch to all listeners. Shouldn't be null.
     * @param paramsJS
     *            the params associated with the event. Can be empty, can be null.
     *  @return false if there is no such listener.
     */
    final boolean dispatchEventTo(final String service, final String event, final OPParamsJS paramsJS) {
        assert service != null : "parameter service shouldn't be null";
        assert event != null : "parameter event shouldn't be null";
        loggerEventDispatch.log("Dispatching event " + event  + " to service " + service + (paramsJS == null ? " with no params" : "with params " + paramsJS.getDescription()));

        return dispatchEventToImpl(service, event, paramsJS);
    }

    /**
     * Real implementation of event dispatching to a specific listeners.
     * 
     * @param serviceName
     *            the name of the listener to dispatch the message.
     * @param event
     *            the event to dispatch to all listeners. Shouldn't be null.
     * @param params
     *            the params associated with the event. Can be empty, can be null.
     * @return true if there is a listener for this service, false otherwise
     */
    private final native boolean dispatchEventToImpl(final String serviceName, final String event, final OPParamsJS params) /*-{
        if (! this.listeners[serviceName]) {
            return false;
        }

        this.listeners[serviceName](event, params);

        return true;
    }-*/;

}
